<?php
require_once 'rpo/util/ObjectStorage.php';
require_once 'rpo/composite/RecursiveComponentIterator.php';
require_once 'rpo/composite/RecursiveComponentIteratorIterator.php';

/**
 * @brief		Component
 * @details		Interface para identificação de um Component
 * @class		Component
 */
abstract class Component extends ObjectStorage implements Countable {
	/**
	 * @brief	Pai de um componente
	 * @var		Component
	 */
	private $father;

	/**
	 * @brief	Identifica uma folha na árvore de componentes
	 * @see		Component.isLeaf()
	 * @var		boolean
	 */
	private $leaf = false;

	/**
	 * @brief	Constroi o componente
	 */
	public function __construct(){
		parent::__construct();

		$this->leaf = $this->isLeaf();
	}

	/**
	 * @brief	Verifica se um objeto é aceitável para o componente antes de adicioná-lo
	 * @param	$object IObject
	 * @return	boolean
	 */
	protected function accept( IObject $object ){
		return $object->getClass()->isSubclassOf( 'Component' );
	}

	/**
	 * @brief	Adiciona um novo filho ao componente
	 * @param	$child Component
	 * @return	Component Este componente
	 * @see		Component.isLeaf()
	 * @throws	LogicException Se tentar adicionar um filho em uma folha
	 * @throws	InvalidArgumentException Se o filho não for aceito pelo filho
	 */
	public function add( Component $child ){
		$this->append( $child );
	}

	/**
	 * @brief	Adiciona um novo objeto
	 * @param	$object IObject
	 * @throws	LogicException Se o componente não puder ter filhos
	 * @throws	UnexpectedValueException Se $object não for um Component
	 * @throws	InvalidArgumentException Se o filho não for aceito pelo componente
	 */
	public function append( IObject $object ){
		if ( !$this->leaf ){
			if ( $object->getClass()->isSubclassOf( 'Component' ) ){
				if ( $this->accept( $object ) ){
					$object->setFather( $this );
					parent::append( $object );
				} else {
					throw new InvalidArgumentException( 'Esse filho não pode ser aceito pelo componente' );
				}
			} else {
				throw new UnexpectedValueException( 'Um componente aceita apenas Component como filho.' );
			}
		} else {
			throw new LogicException( sprintf( 'O componente %s não pode ter filhos' , $this->getClass()->getName() ) );
		}
	}

	/**
	 * @brief	Cria uma instância do Iterator que será utilizado pelo componente
	 * @return	RecursiveComponentIterator
	 * @see		rpo.util.ObjectStorage.createObjectIterator()
	 */
	protected function createObjectIterator(){
		return new RecursiveComponentIterator();
	}

	/**
	 * @brief	Recupera um filho de uma posição específica
	 * @param	$offset integer
	 * @return	Component
	 * @see		Component.isLeaf()
	 * @throws	OutOfBoundsException Se não existir um filho na posição especificada
	 * @throws	LogicException Se tentar recuperar um filho de uma folha
	 */
	public function getChildAt( $offset ){
		if ( !$this->leaf ){
			if ( $this->offsetExists( $offset ) ){
				return $this->offsetGet( $offset );
			} else {
				throw new OutOfBoundsException( 'Não existe um filho para a posição especificada.' );
			}
		} else {
			throw new LogicException( sprintf( 'O componente %s não pode ter filhos' , $this->getClass()->getName() ) );
		}
	}

	/**
	 * @brief	Recupera o pai do componente
	 * @return	Component
	 * @see		Component.hasFather()
	 * @throws	BadMethodCallException Se o componente não tiver um pai
	 */
	public function getFather(){
		if ( $this->hasFather() ){
			return $this->father;
		} else {
			throw new BadMethodCallException( 'Este componente não possui um pai.' );
		}
	}

	/**
	 * @brief	Recupera uma instância de um Iterator recursivo de componentes
	 * @return	RecursiveComponentIterator
	 * @see		rpo.util.ObjectStorage.getIterator()
	 */
	public function getIterator(){
		return parent::getIterator();
	}

	/**
	 * @brief	Recupera um Iterator de iteratores recursivo
	 * @return	RecursiveComponentIteratorIterator
	 * @see		rpo.util.RecursiveObjectIterator.getRecursiveIterator()
	 */
	public function getRecursiveIterator(){
		return $this->getIterator()->getRecursiveIterator();
	}

	/**
	 * @brief	Verifica se o componente possui filhos
	 * @return	boolean
	 */
	public function hasChildren(){
		return $this->count() >= 1;
	}

	/**
	 * @brief	Verifica se o componente possui um pai
	 * @return	boolean
	 * @see		Component.getFather()
	 */
	public function hasFather(){
		return !is_null( $this->father );
	}

	/**
	 * @brief	Verifica se o tipo é uma folha
	 * @return	boolean
	 */
	abstract protected function isLeaf();

	/**
	 * @brief	Remove um filho do componente
	 * @param	$offset integer
	 * @return	Component Este componente
	 * @see		Component.isLeaf()
	 * @throws	OutOfBoundsException Se não existir um filho na posição especificada
	 * @throws	LogicException Se tentar remover um filho de uma folha
	 */
	public function removeChildAt( $offset ){
		if ( !$this->leaf ){
			if ( $this->offsetExists( $offset ) ){
				$this->offsetUnset( $offset );
			} else {
				throw new OutOfBoundsException( 'Não existe um filho para a posição especificada.' );
			}
		} else {
			throw new LogicException( sprintf( 'O componente %s não pode ter filhos' , $this->getClass()->getName() ) );
		}

		return $this;
	}

	/**
	 * @brief	Define o pai de um componente
	 * @param	$father Component
	 * @return	Component Este componente
	 * @throws	LogicException Se o componente já tiver um pai
	 */
	protected function setFather( Component $father ){
		if ( is_null( $this->father ) ){
			$this->father = $father;
		} else {
			throw new LogicException( 'Este componente já possui um pai' );
		}
	}
}